package com.wiringweb.exceptions;

import com.gooddies.reflection.ReflectionUtils;
import com.wiringweb.data.Tree;
import com.wiringweb.data.Tree.Node;
import com.wiringweb.resolution.Resolution;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author sad
 */
public class ExceptionHandler {

    private Map<Object, Tree<ExceptionTreeNode>> controllerExceptionTree = new HashMap<>();

    public ExceptionHandler() {
    }

    private Class[] getClassHierarchy(Class clazz) {
        List<Class> result = new ArrayList<>();
        while (clazz != null) {
            result.add(clazz);
            clazz = clazz.getSuperclass();
        }

        return result.toArray(new Class[result.size()]);
    }

    private void reverseArray(Object[] array) {
        Object temp;
        for (int i = 0; i < array.length / 2; i++) {
            temp = array[i];
            array[i] = array[ array.length - 1 - i];
            array[ array.length - 1 - i] = temp;
        }
    }

    private void validateMethod(Object controller, Method method) {
        Class[] args = method.getParameterTypes();
        String errorMessage = String.format("Exception handler %s of controller %s should have arguments [Throwable, HttpServletRequest, HttpServletResponse, ExceptionSettings]", method.getName(), controller.getClass().getName());
        if (args.length != 4) {
            throw new WiringWebException(errorMessage);
        } else {
            if (!(ReflectionUtils.isSubclass(Throwable.class, args[0])
                    && args[1] == HttpServletRequest.class
                    && args[2] == HttpServletResponse.class
                    && args[3] == ExceptionSettings.class)) {
                throw new WiringWebException(errorMessage);
            }
        }
    }

    private void addExceptionHandler(Object controller, Method method, Class<? extends Throwable> exceptionClass, Class[] unprocessedClassHierarchy, Node<ExceptionTreeNode> root) {
        validateMethod(controller, method);
        for (int i = 0; i < unprocessedClassHierarchy.length; i++) {
            Node<ExceptionTreeNode> newNode;
            ExceptionTreeNode treeNode = new ExceptionTreeNode(unprocessedClassHierarchy[i]);
            newNode = new Node<>(treeNode);
            root.addChild(newNode);
            if (i == unprocessedClassHierarchy.length - 1) {
                treeNode.addHandler(controller, method);
            }

            root = newNode;
        }
    }

    private Tree<ExceptionTreeNode> getTree(Object controller, boolean createIfNotExist) {
        Tree<ExceptionTreeNode> tree = controllerExceptionTree.get(controller);
        if (tree == null) {
            if (createIfNotExist) {
                tree = new Tree<>();
                controllerExceptionTree.put(controller, tree);
            }
        }
        return tree;
    }

    public void addExceptionHandler(Object controller, Method method, Class<? extends Throwable> exceptionClass) {
        Node<ExceptionTreeNode> rootToProcess = getTree(controller, true).getRoot();
        Class[] classHierarhy = getClassHierarchy(exceptionClass);
        reverseArray(classHierarhy);
        int index = 0;

        while (rootToProcess != null) {
            boolean found = false;
            for (Node<ExceptionTreeNode> node : rootToProcess.getChildren()) {
                if (node.getData().getExceptionClass() == exceptionClass) {
                    node.getData().addHandler(controller, method);
                    return;
                } else if (ReflectionUtils.isSubclass(node.getData().getExceptionClass(), exceptionClass)) {
                    rootToProcess = node;
                    index++;
                    found = true;
                    break;
                }
            }

            if (!found) {
                Class[] unprocessedClasses = Arrays.copyOfRange(classHierarhy, index, classHierarhy.length);
                addExceptionHandler(controller, method, exceptionClass, unprocessedClasses, rootToProcess);
                return;
            }
        }
    }

    private ExceptionTreeNode[] searchProcessingPath(Object controller, Throwable throwable) {
        List<ExceptionTreeNode> foundProcessors = new ArrayList<>();
        Node<ExceptionTreeNode> rootToProcess;
        Tree<ExceptionTreeNode> tree = getTree(controller, false);
        if (tree != null) {
            rootToProcess = tree.getRoot();
            while (rootToProcess != null) {
                boolean found = false;
                for (Node<ExceptionTreeNode> node : rootToProcess.getChildren()) {
                    if (node.getData().getExceptionClass() == throwable.getClass()) {
                        if (!node.getData().getHandlers().isEmpty()) {
                            foundProcessors.add(node.getData());
                        }
                        break;
                    } else if (ReflectionUtils.isSubclass(node.getData().getExceptionClass(), throwable.getClass())) {
                        if (!node.getData().getHandlers().isEmpty()) {
                            foundProcessors.add(node.getData());
                        }
                        rootToProcess = node;
                        found = true;
                        break;
                    }
                }

                if (found == false) {
                    break;
                }
            }
        }

        ExceptionTreeNode[] array = foundProcessors.toArray(new ExceptionTreeNode[foundProcessors.size()]);
        reverseArray(array);
        return array;
    }

    public Resolution processException(Throwable exception, Object controller, HttpServletRequest request, HttpServletResponse response) {
        ExceptionSettings settings = new ExceptionSettings();
        ExceptionTreeNode[] foundProcessors = searchProcessingPath(controller, exception);
        Object[] args = new Object[]{exception, request, response, settings};
        boolean stop = false;
        for (ExceptionTreeNode exceptionProcessor : foundProcessors) {
            if (stop) {
                break;
            }

            for (ExceptionTreeNode.ExceptionHandlerObject ho : exceptionProcessor.getHandlers()) {
                try {
                    ho.getMethod().invoke(ho.getController(), args);
                } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
                    throw new WiringWebException("Exception in exception handler", ex);
                }
                if (settings.isStopProcessing()) {
                    stop = true;
                    break;
                }
            }
        }

        return settings.getResolution();
    }
}